package com.googlecode.rulio;

import com.googlecode.totallylazy.Callable1;
import com.googlecode.totallylazy.Pair;
import com.googlecode.totallylazy.Predicate;
import com.googlecode.totallylazy.Sequence;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static com.googlecode.totallylazy.Pair.pair;
import static com.googlecode.totallylazy.Predicates.always;
import static com.googlecode.totallylazy.Sequences.sequence;
import static java.util.Arrays.asList;

public class Rules<X> extends Action<X, ExecutionReport> {
    private List<Pair<Predicate, List<ActivatingAction>>> rules = new ArrayList<Pair<Predicate, List<ActivatingAction>>>();

    private ActionActivator activator;
    private boolean executeAllMatching;

    public Rules(boolean executeAllMatching) {
        this.executeAllMatching = executeAllMatching;
    }

    public Rules(ActionActivator activator, boolean executeAllMatching) {
        this(executeAllMatching);
        this.activator = activator;
    }

    private Rules(ActionActivator activator, List<Pair<Predicate, List<ActivatingAction>>> rules, boolean executeAllMatching) {
        this.activator = activator;
        this.rules = new ArrayList<Pair<Predicate, List<ActivatingAction>>>(rules);
        this.executeAllMatching = executeAllMatching;
    }


    public <T extends X> Rules<X> add(Predicate<T> predicate,
                                      ActivatingAction<? extends Action<? super T, ?>> action) {
        return add(predicate, new ActivatingAction[]{action});
    }

    public <T extends X> Rules<X> add(Predicate<T> predicate,
                                      ActivatingAction<? extends Action<? super T, ?>> action1,
                                      ActivatingAction<? extends Action<? super T, ?>> action2) {
        return add(predicate, new ActivatingAction[]{action1, action2});
    }

    public <T extends X> Rules<X> add(Predicate<T> predicate,
                                      ActivatingAction<? extends Action<? super T, ?>> action1,
                                      ActivatingAction<? extends Action<? super T, ?>> action2,
                                      ActivatingAction<? extends Action<? super T, ?>> action3) {
        return add(predicate, new ActivatingAction[]{action1, action2, action3});
    }

    public <T extends X> Rules<X> add(Predicate<T> predicate,
                                      ActivatingAction<? extends Action<? super T, ?>> action1,
                                      ActivatingAction<? extends Action<? super T, ?>> action2,
                                      ActivatingAction<? extends Action<? super T, ?>> action3,
                                      ActivatingAction<? extends Action<? super T, ?>> action4) {
        return add(predicate, new ActivatingAction[]{action1, action2, action3, action4});
    }

    private <T extends X> Rules<X> add(Predicate<T> predicate, ActivatingAction[] actions) {
        rules.add(pair((Predicate) predicate, asList(actions)));
        return this;
    }

    public Action<X, ExecutionReport> activate(final ActionActivator activator) throws Exception {
        return new Rules<X>(activator, rules, executeAllMatching);
    }


    public ExecutionReport execute(final X object) throws Exception {
        Object[] executionReports;
        if (executeAllMatching()) {
            executionReports = sequence(rules)
                    .map(executeRule(object))
                    .toArray(Object.class);
        } else {
            executionReports = sequence(rules)
                    .filter(ruleMatching(object))
                    .take(1)
                    .map(executeRule(object))
                    .toArray(Object.class);
        }
        return new ExecutionReport(this, always(), executionReports, true, true, null);
    }

    @SuppressWarnings("unchecked")
    private Predicate<? super Pair<Predicate, List<ActivatingAction>>> ruleMatching(final X object) {
        return new com.googlecode.rulio.Predicate<Pair<Predicate, List<ActivatingAction>>>() {
            public boolean matches(Pair<Predicate, List<ActivatingAction>> other) {
                return other.first().matches(object);
            }
        };
    }

    @SuppressWarnings("unchecked")
    private Callable1<Pair<Predicate, List<ActivatingAction>>, ExecutionReport> executeRule(final X object) {
        return new Callable1<Pair<Predicate, List<ActivatingAction>>, ExecutionReport>() {
            public ExecutionReport call(Pair<Predicate, List<ActivatingAction>> rule) throws Exception {
                Sequence<Pair<Predicate, Action>> actions = activateActions(sequence(rule.second()));
                if (ignoreClassCastException(rule.first()).matches(object)) {
                    Sequence<Object> actionResults = actions.map(executeAction(object));
                    return new ExecutionReport(actions.toList(), rule.first(), actionResults.toArray(Object.class), true, true, null);
                } else {
                    return new ExecutionReport(actions.toList(), rule.first(), new Object[0], false, false, null);
                }
            }
        };
    }

    @SuppressWarnings("unchecked")
    private Callable1<Pair<Predicate, Action>, Object> executeAction(final X object) {
        return new Callable1<Pair<Predicate, Action>, Object>() {
            public Object call(Pair<Predicate, Action> predicateActionPair) throws Exception {
                if (ignoreClassCastException(predicateActionPair.first()).matches(object)) {
                    try {
                        Object result = predicateActionPair.second().execute(object);
                        if (result instanceof ExecutionReport) {
                            return result;
                        } else {
                            return new ExecutionReport(predicateActionPair.second(), predicateActionPair.first(), new Object[]{result}, true, true, null);
                        }
                    } catch (Exception e) {
                        return new ExecutionReport(predicateActionPair.second(), predicateActionPair.first(), new Object[0], true, false, e);
                    }

                } else {
                    return new ExecutionReport(predicateActionPair.second(), predicateActionPair.first(), new Object[]{}, false, false, null);
                }

            }
        };
    }


    public <T> Predicate<T> ignoreClassCastException(final Predicate<T> predicate) {
        return new Predicate<T>() {
            public boolean matches(T object) {
                try {
                    return predicate.matches(object);
                } catch (ClassCastException e) {
                    return false;
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }


    private Sequence<Pair<Predicate, Action>> activateActions(Sequence<ActivatingAction> actionsToActivate) {
        return actionsToActivate.map(toAction());

    }

    private Callable1<ActivatingAction, Pair<Predicate, Action>> toAction() {
        return new Callable1<ActivatingAction, Pair<Predicate, Action>>() {
            public Pair<Predicate, Action> call(ActivatingAction activatingAction) throws Exception {
                return pair(activatingAction.predicate(), activatingAction.activate(activator));
            }
        };
    }

    public boolean executeAllMatching() {
        return executeAllMatching;
    }

    public static <R extends Action> ActivatingAction<R> action(final Class<R> actionClass, final Object... dependencies) {
        return new ActivatingAction<R>() {
            public R activate(ActionActivator activator) throws Exception {
                return activator.activate(actionClass, dependencies);
            }

            public Predicate predicate() {
                return always();
            }
        };

    }

    public static <R extends Action, P> ActivatingAction<R> conditionalAction(final Predicate<P> predicate, final ActivatingAction<R> action) {
        return new ActivatingAction<R>() {
            public R activate(ActionActivator activator) throws Exception {
                return action.activate(activator);
            }

            public Predicate predicate() {
                return predicate;
            }
        };

    }
}
